package com.qf.service.impl;

import com.feign.CityFeign;
import com.feign.HotalFeign;
import com.qf.entity.*;
import com.qf.service.ISearchService;
import org.apache.lucene.search.join.ScoreMode;
import org.elasticsearch.common.lucene.search.function.CombineFunction;
import org.elasticsearch.index.query.*;
import org.elasticsearch.index.query.functionscore.FieldValueFactorFunctionBuilder;
import org.elasticsearch.index.query.functionscore.FunctionScoreQueryBuilder;
import org.elasticsearch.index.query.functionscore.ScoreFunctionBuilders;
import org.elasticsearch.script.Script;
import org.elasticsearch.script.ScriptType;
import org.elasticsearch.search.sort.ScriptSortBuilder;
import org.elasticsearch.search.sort.SortBuilder;
import org.elasticsearch.search.sort.SortBuilders;
import org.elasticsearch.search.sort.SortOrder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.elasticsearch.core.ElasticsearchRestTemplate;
import org.springframework.data.elasticsearch.core.IndexOperations;
import org.springframework.data.elasticsearch.core.SearchHits;
import org.springframework.data.elasticsearch.core.document.Document;
import org.springframework.data.elasticsearch.core.mapping.IndexCoordinates;
import org.springframework.data.elasticsearch.core.query.*;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;

import java.text.DecimalFormat;
import java.text.NumberFormat;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

@Service
public class SearchServiceImpl implements ISearchService {

    @Autowired
    private ElasticsearchRestTemplate restTemplate;

    @Autowired
    private HotalFeign hotalFeign;

    @Autowired
    private CityFeign cityFeign;

    /**
     * 平均价格的自定义字段脚本
     */
    private String avgPriceScrpit = "//标志位\n" +
            "          boolean flag = true;\n" +
            "          //总平均价格\n" +
            "          double avgPrice = Integer.MAX_VALUE;\n" +
            "          \n" +
            "          //循环当前酒店的所有客房\n" +
            "          for(room in params._source.rooms) {\n" +
            "          \t//重置标志位\n" +
            "          \tflag = true;\n" +
            "          \t//房间的总价格\n" +
            "          \tdouble allPrice = 0;\n" +
            "          \t//总天数\n" +
            "          \tint days = 0;\n" +
            "          \n" +
            "          \t//循环每间客房中的价格信息\n" +
            "          \tfor(price in room.roomPrices){\n" +
            "          \t\t//判断当前的价格日期是否在用户需要的入住时间和离店时间范围内\n" +
            "          \t\tif(price.date >= params.beginTime && price.date < params.endTime){\n" +
            "          \n" +
            "          \t\t\t//当前房间这一天是否已经住满\n" +
            "          \t\t\tif(price.number == price.hasNumber){\n" +
            "          \t\t\t\tflag = false;\n" +
            "          \t\t\t}\n" +
            "          \n" +
            "          \t\t\t//价格的累加\n" +
            "          \t\t\tallPrice += price.price;\n" +
            "          \t\t\t//累加天数\n" +
            "          \t\t\tdays++;\n" +
            "          \t\t}\n" +
            "          \t}\n" +
            "          \n" +
            "          \t//计算平均价格\n" +
            "          \tif(flag){\n" +
            "          \t\t//总价格 / 天数 = 平均价\n" +
            "          \t\tdouble avgp = allPrice / days;\n" +
            "          \t\tif(avgp >= params.minPrice && avgp <= params.maxPrice){\n" +
            "          \t\t\tavgPrice = Math.min(avgPrice, avgp);\n" +
            "          \t\t}\n" +
            "          \t}\n" +
            "          }\n" +
            "          \n" +
            "          //返回平均价\n" +
            "          return avgPrice == Integer.MAX_VALUE ? -1 : avgPrice;";

    /**
     * 计算距离的脚本
     */
    private String distanceScript = "doc.location.planeDistance(params.lat, params.lon) / 1000";

    /**
     * 创建索引库
     * @return
     */
    @Override
    public boolean createIndex() {
        IndexOperations operations = restTemplate.indexOps(Hotal.class);
        //判断是否存在索引库
        if (!operations.exists()) {
            //创建索引库
            boolean flag = operations.create();
            //构建映射关系
            createMapping();

            //同步一次数据库
            syncDataBase();

            System.out.println("索引库创建成功！");
            return flag;
        }
        return true;
    }

    /**
     * 创建映射类型
     * @return
     */
    @Override
    public boolean createMapping() {
        IndexOperations operations = restTemplate.indexOps(Hotal.class);
        Document mapping = operations.createMapping();
        boolean flag = operations.putMapping(mapping);
        return flag;
    }

    /**
     * 删除索引库
     * @return
     */
    @Override
    public boolean deleteIndex() {
        IndexOperations operations = restTemplate.indexOps(Hotal.class);
        boolean delete = operations.delete();
        return delete;
    }

    @Override
    public boolean insertHotal(Hotal hotal) {
        City city = cityFeign.getCityById(hotal.getCid()).getData();
        hotal.setCity(city);
        Hotal hotal1 = restTemplate.save(hotal);
        return hotal1 != null;
    }

    @Override
    public boolean insertRooms(Room room) {

        //将room对象转成Map集合
        Map<String, Object> roomMap = new HashMap<>();
        roomMap.put("id", room.getId());
        roomMap.put("hid", room.getHid());
        roomMap.put("number", room.getNumber());
        roomMap.put("title", room.getTitle());
        roomMap.put("roomPrices", new ArrayList<>());

        Map<String, Object> map = new HashMap<>();
        map.put("room", roomMap);

        //参数为修改的文档id
        UpdateQuery updateQuery = UpdateQuery.builder(room.getHid() + "")
                .withScript("ctx._source.rooms.add(params.room)")
                .withParams(map)
                .build();

        //修改文档
        UpdateResponse response = restTemplate.update(updateQuery, IndexCoordinates.of("hotal_index"));

        //新增客房价格信息
        insertRoomPrices(room.getHid(), room.getRoomPrices());
        return true;
    }

    @Override
    public boolean insertRoomPrices(Integer hid, List<RoomPrice> roomPrices) {

        //循环新增客房的价格信息
        roomPrices.stream().forEach(roomPrice -> {

            //将房间价格信息转换成Map集合
            Map<String, Object> roomPriceMap = new HashMap<>();
            roomPriceMap.put("id", roomPrice.getId());
            roomPriceMap.put("rid", roomPrice.getRid());
            roomPriceMap.put("date", roomPrice.getDate().getTime());
            roomPriceMap.put("number", roomPrice.getNumber());
            roomPriceMap.put("hasNumber", roomPrice.getHasNumber());
            roomPriceMap.put("price", roomPrice.getPrice());

            Map<String, Object> paramsMap = new HashMap<>();
            paramsMap.put("rid", roomPrice.getRid());
            paramsMap.put("roomPrice", roomPriceMap);

            UpdateQuery updateQuery = UpdateQuery.builder(hid + "")
                    .withScript("for(int i = 0; i < ctx._source.rooms.length; i++){" +
                            "if(ctx._source.rooms[i].id == params.rid){" +
                            "ctx._source.rooms[i].roomPrices.add(params.roomPrice)" +
                            "}" +
                            "}")
                    .withParams(paramsMap)
                    .build();

            //逐个修改客房的日期价格
            restTemplate.update(updateQuery, IndexCoordinates.of("hotal_index"));

        });

        return true;
    }

    @Override
    public boolean updateRoomPrice(Integer hid, RoomPrice roomPrice) {

        Map<String, Object> paramsMap = new HashMap<>();
        paramsMap.put("rid", roomPrice.getRid());
        paramsMap.put("rpid", roomPrice.getId());
        paramsMap.put("price", roomPrice.getPrice());

        UpdateQuery updateQuery = UpdateQuery.builder(hid + "")
                .withScript("for(r in ctx._source.rooms){" +
                            "if(r.id == params.rid){" +
                            "for(rp in r.roomPrices){" +
                            "if(rp.id == params.rpid){" +
                            "rp.price = params.price" +
                            "}}}}")
                .withParams(paramsMap)
                .build();

        restTemplate.update(updateQuery, IndexCoordinates.of("hotal_index"));
        return true;
    }

    /**
     * 将数据库的hotal表中所有的酒店信息，同步到ES索引库中
     */
    @Override
    public void syncDataBase() {
        //酒店列表
        List<Hotal> hotals = hotalFeign.queryList().getData();

        //循环酒店列表保存到ES中
        hotals.stream().forEach(hotal -> {
            //查询酒店对应的城市信息
            City city = cityFeign.getCityById(hotal.getCid()).getData();
            hotal.setCity(city);
            insertHotal(hotal);
        });
    }

    /**
     * 搜索酒店
     *
     * 1、关键词应该匹配哪些字段？
     * 2、入店时间和离店时间有什么用？ - 如果在这个时间范围内，酒店所有客房都已经预订满了，则该酒店就不会被搜索出来
     *      希尔顿酒店 - Document
     *        - 豪华单人间 - 均价 - 201
     *          - 2020-11-24 共10 已预订8间  199
     *          - 2020-11-25 共10 已预订10间 168
     *          - ....
     *        - 经济总统套房 - 均价 - 2186
     *          - 2020-11-24 共1  已预订0间  1999
     *          - 2020-11-25 共1  已预订0间  2005
     *          ....
     * 3、最小价格和最大价格的作用？ - 限制酒店预订的价格范围 min 1000 max 200000
     *      - 限制酒店客房的平均价格
     *      - 显示价格区间范围内的最低均价
     *
     * 4、城市名称的作用？ - 搜索所有城市的符合条件的酒店信息，如果是当前城市的酒店，评分会自动提升
     *
     * 5、经纬度 - 计算城市到人的直线距离
     *
     * @param searchParams
     * @return
     */
    @Override
    public List<Hotal> searchHotal(SearchParams searchParams) {

        //日期范围查询
        RangeQueryBuilder rangeQueryBuilder = QueryBuilders.rangeQuery("rooms.roomPrices.date")
                .gte(searchParams.getBeginTime().getTime())
                .lt(searchParams.getEndTime().getTime());

        //脚本查询
        ScriptQueryBuilder scriptQuery = QueryBuilders.scriptQuery(new Script(
                "doc['rooms.roomPrices.number'].value == doc['rooms.roomPrices.hasNumber'].value"
        ));

        //positive查询 - must查询
        NestedQueryBuilder positiveQuery = QueryBuilders.nestedQuery("rooms",
                QueryBuilders.boolQuery().mustNot(
                        QueryBuilders.nestedQuery("rooms.roomPrices",
                                QueryBuilders.boolQuery()
                                        .must(rangeQueryBuilder)
                                        .must(scriptQuery)
                                , ScoreMode.Avg)),
                ScoreMode.Avg);


        //positive查询
        BoolQueryBuilder keywordQuery = QueryBuilders.boolQuery();

        //用户入店和离店时间不为空
        if (searchParams.getBeginTime() != null && searchParams.getEndTime() != null) {
            keywordQuery
                    .must(positiveQuery);
        }

        //用户关键词不为空
        if (!StringUtils.isEmpty(searchParams.getKeyword())) {
            keywordQuery
                .should(QueryBuilders.multiMatchQuery(searchParams.getKeyword(), "hotalName", "hotalName.pinyin", "hotalInfo", "keyword", "brand", "address", "regstr"))
                .should(QueryBuilders.nestedQuery("rooms", QueryBuilders.matchQuery("rooms.title", searchParams.getKeyword()), ScoreMode.Avg))
                .minimumShouldMatch(1);
        }

        //negative查询 - 嵌套查询城市名称
        NestedQueryBuilder cityQuery = QueryBuilders.nestedQuery("city",
                QueryBuilders.termQuery("city.cityName.keyword", searchParams.getCityName()), ScoreMode.Avg);

        //执行查询
        BoostingQueryBuilder execQuery = QueryBuilders
                .boostingQuery(keywordQuery, cityQuery)
                .negativeBoost(10);

        //评分搜索
        FieldValueFactorFunctionBuilder functionBuilder = ScoreFunctionBuilders.fieldValueFactorFunction("clicks").setWeight(5);
        FunctionScoreQueryBuilder scorequery = QueryBuilders.functionScoreQuery(execQuery, functionBuilder)
                .boostMode(CombineFunction.MULTIPLY);

        //--------------------------设置自定义字段--------------------------
        Map<String, Object> params = new HashMap<>();
        params.put("beginTime", searchParams.getBeginTime() != null ? searchParams.getBeginTime().getTime() : 0);
        params.put("endTime", searchParams.getEndTime() != null ? searchParams.getEndTime().getTime() : Long.MAX_VALUE);
        params.put("minPrice", searchParams.getMinPrice() != null ? searchParams.getMinPrice() : 0);
        params.put("maxPrice", searchParams.getMaxPrice() != null ? searchParams.getMaxPrice() : Integer.MAX_VALUE);
        params.put("lon", searchParams.getLon());
        params.put("lat", searchParams.getLat());

        //平均价格的自定义脚本字段
        ScriptField avgPrice = new ScriptField("avgPrice", new Script(
                ScriptType.INLINE, "painless", avgPriceScrpit, params
        ));

        //自定义距离的脚本字段
        ScriptField distance = new ScriptField("distance", new Script(
                ScriptType.INLINE, "painless", distanceScript, params
        ));

        //--------------------------排序的方式---------------------
        List<SortBuilder> sortBuilders = new ArrayList<>();
        System.out.println("排序方式：" + searchParams.getSortType());
        switch (searchParams.getSortType()) {
            case 0:
                //智能排序 - 评分排序
                sortBuilders.add(SortBuilders.scoreSort());
                break;
            case 1:
                //价格排序
                sortBuilders.add(SortBuilders.scriptSort(new Script(
                        ScriptType.INLINE, "painless", avgPriceScrpit, params
                ), ScriptSortBuilder.ScriptSortType.NUMBER).order(SortOrder.ASC));
                break;
            case 2:
                //离我最近
                sortBuilders.add(SortBuilders.scriptSort(new Script(
                        ScriptType.INLINE, "painless", distanceScript, params
                ), ScriptSortBuilder.ScriptSortType.NUMBER).order(SortOrder.ASC));
                break;
        }

        //执行查询方法
        return search(scorequery, sortBuilders, avgPrice, distance);
    }


    /**
     * 执行查询的方法
     * @return
     */
    private List<Hotal> search(QueryBuilder queryBuilder, List<SortBuilder> sortBuilders, ScriptField ... scriptFields){

        //创建原生的查询对象
        NativeSearchQuery nativeSearchQuery = new NativeSearchQuery(queryBuilder, null, sortBuilders);

        //如果有自定义字段
        if (scriptFields != null) {
            //展示数据时排除rooms属性
            String[] exclude = {"rooms"};
            SourceFilter sourceFilter = new FetchSourceFilter(null, exclude);
            nativeSearchQuery.addSourceFilter(sourceFilter);
            //设置自定义字段
            nativeSearchQuery.addScriptField(scriptFields);
        }

        List<Hotal> hotals = new ArrayList<>();
        //执行搜索
        SearchHits<Hotal> search = restTemplate.search(nativeSearchQuery, Hotal.class);
        System.out.println("查询出的结果数量：" + search.getTotalHits());
        //循环搜索结果
        search.stream().forEach(hotalSearchHit -> {
            //搜索出来的酒店信息
            Hotal hotal = hotalSearchHit.getContent();
            if (hotal.getAvgPrice() != -1) {
                //处理平均价格的格式问题
                NumberFormat numberFormat = new DecimalFormat("###.##");
                hotal.setAvgPrice(Double.valueOf(numberFormat.format(hotal.getAvgPrice())));
                //处理距离的格式化问题
                hotal.setDistance(Double.valueOf(numberFormat.format(hotal.getDistance())));
                hotals.add(hotal);
            }

            System.out.println("搜索出来的结果：" + hotal);
        });
        //返回搜索结果
        return hotals;
    }

    /**
     * 修改酒店点击率
     * @param hid
     * @param clicks
     * @return
     */
    @Override
    public int updateHotalClicks(Integer hid, Integer clicks) {
        Map<String,Object> params = new HashMap<>();
        params.put("clicks", clicks);

        UpdateQuery updateQuery = UpdateQuery.builder(hid + "")
                .withScript("ctx._source.clicks+=params.clicks")
                .withParams(params)
                .build();

        restTemplate.update(updateQuery, IndexCoordinates.of("hotal_index"));
        return 1;
    }
}
